/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.coyote.ajp;

import java.net.InetAddress;
import java.util.regex.Pattern;

import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Processor;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.UpgradeToken;
import org.apache.tomcat.util.net.AbstractEndpoint;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.apache.tomcat.util.res.StringManager;

/**
 * The is the base implementation for the AJP protocol handlers. Implementations typically extend this base class rather
 * than implement {@link org.apache.coyote.ProtocolHandler}. All of the implementations that ship with Tomcat are
 * implemented this way.
 *
 * @param <S>
 *            The type of socket used by the implementation
 */
public abstract class AbstractAjpProtocol<S> extends AbstractProtocol<S> {

	/**
	 * The string manager for this package.
	 */
	protected static final StringManager sm = StringManager.getManager(AbstractAjpProtocol.class);

	public AbstractAjpProtocol(AbstractEndpoint<S, ?> endpoint) {
		super(endpoint);
		setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
		// AJP does not use Send File
		getEndpoint().setUseSendfile(false);
		// AJP listens on loopback by default
		getEndpoint().setAddress(InetAddress.getLoopbackAddress());
		ConnectionHandler<S> cHandler = new ConnectionHandler<>(this);
		setHandler(cHandler);
		getEndpoint().setHandler(cHandler);
	}

	@Override
	protected String getProtocolName() {
		return "Ajp";
	}

	/**
	 * {@inheritDoc}
	 *
	 * Overridden to make getter accessible to other classes in this package.
	 */
	@Override
	protected AbstractEndpoint<S, ?> getEndpoint() {
		return super.getEndpoint();
	}

	/**
	 * {@inheritDoc}
	 *
	 * AJP does not support protocol negotiation so this always returns null.
	 */
	@Override
	protected UpgradeProtocol getNegotiatedProtocol(String name) {
		return null;
	}

	/**
	 * {@inheritDoc}
	 *
	 * AJP does not support protocol upgrade so this always returns null.
	 */
	@Override
	protected UpgradeProtocol getUpgradeProtocol(String name) {
		return null;
	}

	// ------------------------------------------------- AJP specific properties
	// ------------------------------------------ managed in the ProtocolHandler

	private boolean ajpFlush = true;

	public boolean getAjpFlush() {
		return ajpFlush;
	}

	/**
	 * Configure whether to aend an AJP flush packet when flushing. A flush packet is a zero byte AJP13 SEND_BODY_CHUNK
	 * packet. mod_jk and mod_proxy_ajp interpret this as a request to flush data to the client. AJP always does flush
	 * at the and of the response, so if it is not important, that the packets get streamed up to the client, do not use
	 * extra flush packets. For compatibility and to stay on the safe side, flush packets are enabled by default.
	 *
	 * @param ajpFlush
	 *            The new flush setting
	 */
	public void setAjpFlush(boolean ajpFlush) {
		this.ajpFlush = ajpFlush;
	}

	private boolean tomcatAuthentication = true;

	/**
	 * Should authentication be done in the native web server layer, or in the Servlet container ?
	 *
	 * @return {@code true} if authentication should be performed by Tomcat, otherwise {@code false}
	 */
	public boolean getTomcatAuthentication() {
		return tomcatAuthentication;
	}

	public void setTomcatAuthentication(boolean tomcatAuthentication) {
		this.tomcatAuthentication = tomcatAuthentication;
	}

	private boolean tomcatAuthorization = false;

	/**
	 * Should authentication be done in the native web server layer and authorization in the Servlet container?
	 *
	 * @return {@code true} if authorization should be performed by Tomcat, otherwise {@code false}
	 */
	public boolean getTomcatAuthorization() {
		return tomcatAuthorization;
	}

	public void setTomcatAuthorization(boolean tomcatAuthorization) {
		this.tomcatAuthorization = tomcatAuthorization;
	}

	private String secret = null;

	/**
	 * Set the secret that must be included with every request.
	 *
	 * @param secret
	 *            The required secret
	 */
	public void setSecret(String secret) {
		this.secret = secret;
	}

	protected String getSecret() {
		return secret;
	}

	/**
	 * Set the required secret that must be included with every request.
	 *
	 * @param requiredSecret
	 *            The required secret
	 *
	 * @deprecated Replaced by {@link #setSecret(String)}. Will be removed in Tomcat 11 onwards
	 */
	@Deprecated
	public void setRequiredSecret(String requiredSecret) {
		setSecret(requiredSecret);
	}

	/**
	 * @return The current secret
	 *
	 * @deprecated Replaced by {@link #getSecret()}. Will be removed in Tomcat 11 onwards
	 */
	@Deprecated
	protected String getRequiredSecret() {
		return getSecret();
	}

	private boolean secretRequired = true;

	public void setSecretRequired(boolean secretRequired) {
		this.secretRequired = secretRequired;
	}

	public boolean getSecretRequired() {
		return secretRequired;
	}

	private Pattern allowedRequestAttributesPattern;

	public void setAllowedRequestAttributesPattern(String allowedRequestAttributesPattern) {
		this.allowedRequestAttributesPattern = Pattern.compile(allowedRequestAttributesPattern);
	}

	public String getAllowedRequestAttributesPattern() {
		return allowedRequestAttributesPattern.pattern();
	}

	protected Pattern getAllowedRequestAttributesPatternInternal() {
		return allowedRequestAttributesPattern;
	}

	/**
	 * AJP packet size.
	 */
	private int packetSize = Constants.MAX_PACKET_SIZE;

	public int getPacketSize() {
		return packetSize;
	}

	public void setPacketSize(int packetSize) {
		if (packetSize < Constants.MAX_PACKET_SIZE) {
			this.packetSize = Constants.MAX_PACKET_SIZE;
		} else {
			this.packetSize = packetSize;
		}
	}

	@Override
	public int getDesiredBufferSize() {
		return getPacketSize() - Constants.SEND_HEAD_LEN;
	}

	// --------------------------------------------- SSL is not supported in AJP

	@Override
	public void addSslHostConfig(SSLHostConfig sslHostConfig) {
		getLog().warn(sm.getString("ajpprotocol.noSSL", sslHostConfig.getHostName()));
	}

	@Override
	public SSLHostConfig[] findSslHostConfigs() {
		return new SSLHostConfig[0];
	}

	@Override
	public void addUpgradeProtocol(UpgradeProtocol upgradeProtocol) {
		getLog().warn(sm.getString("ajpprotocol.noUpgrade", upgradeProtocol.getClass().getName()));
	}

	@Override
	public UpgradeProtocol[] findUpgradeProtocols() {
		return new UpgradeProtocol[0];
	}

	@Override
	protected Processor createProcessor() {
		AjpProcessor processor = new AjpProcessor(this, getAdapter());
		return processor;
	}

	@Override
	protected Processor createUpgradeProcessor(SocketWrapperBase<?> socket, UpgradeToken upgradeToken) {
		throw new IllegalStateException(sm.getString("ajpprotocol.noUpgradeHandler",
				upgradeToken.getHttpUpgradeHandler().getClass().getName()));
	}

	@Override
	public void start() throws Exception {
		if (getSecretRequired()) {
			String secret = getSecret();
			if (secret == null || secret.length() == 0) {
				throw new IllegalArgumentException(sm.getString("ajpprotocol.noSecret"));
			}
		}
		super.start();
	}
}
